/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.nViron;

import com.sun.opengl.util.j2d.TextRenderer;
import java.awt.Font;
import java.awt.geom.Rectangle2D;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.glu.GLU;
import javax.media.opengl.glu.GLUquadric;
import org.nViron.nvData.nvDataEntity;
import org.nViron.nvOrganic.nvLeaf;
import org.nViron.nvOrganic.nvNode;
import org.nViron.nvOrganic.nvOrganic;
import org.nViron.nvOrganic.nvRoot;
import org.nViron.nvOrganic.nvStalk;
import org.nViron.nvOrganic.nvStem;
import org.nViron.nvOrganism.nvStateMgr;
import org.nViron.nvParams.nvVizParamMgr.VIZPARAM;
import org.nViron.nvPlaces.nvPlace;
import org.nViron.nvProjection.nvMercator;
import org.nViron.nvRender.nvFrustum;
import org.nViron.nvRender.nvRenderer;
import org.nViron.nvRender.nvVec3;
import org.nViron.nvSim.nvSimulator;
import org.nViron.nvTraces.nvTrace;

/**
 *
 * @author Roland
 */
public class SimpleRenderer extends nvRenderer {

    private int sphereList1 = -1;
    private int sphereList2 = -1;
    private int sphereList3 = -1;
    private int sphereList4 = -1;
    private TextRenderer textrenderer2d;
    private TextRenderer textrenderer3d;

    private GL gl;
    private GLU glu;
    private GLUquadric gluQuadric;
    private int framecount;
    private float fps;
    private GLAutoDrawable drawable;
    private long timestamp = System.currentTimeMillis();

    public SimpleRenderer(nvSimulator simulator, GLAutoDrawable _drawable) {
        super(simulator);
        setRenderGrid(true);
        setRenderDebugInfo(true);

        drawable = _drawable;
        gl = drawable.getGL();
        glu = new GLU();
    }

    @Override
    protected nvFrustum createFrustum() {
        return new nvFrustum();
    }

    @Override
    public void onReshape(int x, int y, int width, int height) {
        gl.glViewport(0, 0, width, height);
        final float aspect = (float) width / (float) height;
        gl.glMatrixMode(gl.GL_PROJECTION);
        gl.glLoadIdentity();
        glu.gluPerspective(frustum.getFOV(), aspect, frustum.getNearP(), frustum.getFarP());
        gl.glMatrixMode(gl.GL_MODELVIEW);
        gl.glLoadIdentity();
        frustum.setProjection(frustum.getFOV(), aspect, frustum.getNearP(), frustum.getFarP());
    }

    @Override
    protected void renderBackground() {
    }

    @Override
    protected void setupCamera() {
        nvMercator mercator = (nvMercator) simulator.getProjection();
        mercator.setCameraPos(simulator.getCameraPosition());

        gl.glMatrixMode(gl.GL_MODELVIEW);
        gl.glLoadIdentity();

        nvVec3 p = mercator.geoToViz(simulator.getCameraPosition());
        nvVec3 l = mercator.geoToViz(simulator.getLookAtPosition());
        nvVec3 u = new nvVec3(0, 1, 0);
        glu.gluLookAt(p.x, p.y, p.z, l.x, l.y, l.z, u.x, u.y, u.z);
        frustum.setCamera(p, l, u);
    }

    @Override
    protected void renderMap() {
    }

    @Override
    protected void renderGrid() {
        if (!frustum.doesIntersectGround()) {
            return;
        }

        disableLighting();

        gl.glDisable(gl.GL_DEPTH_TEST);
        gl.glEnable(gl.GL_BLEND);

        // render frustum ground intersection
        gl.glColor4f(0.75f, 0.75f, 0.0f, 0.75f);
        gl.glBegin(gl.GL_LINES);
        gl.glVertex3f(frustum.gnl.x, 0, frustum.gnl.z);
        gl.glVertex3f(frustum.gnr.x, 0, frustum.gnr.z);
        gl.glVertex3f(frustum.gfr.x, 0, frustum.gfr.z);
        gl.glVertex3f(frustum.gfl.x, 0, frustum.gfl.z);
        gl.glEnd();

        // render lines on frustum ground intersection
        gl.glColor4f(0.25f, 0.25f, 0.25f, 1.0f);
        gl.glBegin(gl.GL_LINES);

        nvVec3 min = new nvVec3(Math.min(frustum.gnl.x, Math.min(frustum.gnr.x, Math.min(frustum.gfr.x, frustum.gfl.x))),
                0.0f,
                Math.min(frustum.gnl.z, Math.min(frustum.gnr.z, Math.min(frustum.gfr.z, frustum.gfl.z))));
        nvVec3 max = new nvVec3(Math.max(frustum.gnl.x, Math.max(frustum.gnr.x, Math.max(frustum.gfr.x, frustum.gfl.x))),
                0.0f,
                Math.max(frustum.gnl.z, Math.max(frustum.gnr.z, Math.max(frustum.gfr.z, frustum.gfl.z))));

        float steps = 25.0f;
        float deltaX = (max.x - min.x) / steps;
        for (int x = 0; x <= steps; x++) {
            gl.glVertex3f(min.x + deltaX * x, 0, min.z);
            gl.glVertex3f(min.x + deltaX * x, 0, max.z);
        }

        float deltaZ = (max.z - min.z) / steps;
        for (int z = 0; z < steps; z++) {
            gl.glVertex3f(min.x, 0, min.z + deltaZ * z);
            gl.glVertex3f(max.x, 0, min.z + deltaZ * z);
        }
        gl.glEnd();
    }

    @Override
    protected void renderForeground() {
        textrenderer2d.beginRendering(drawable.getWidth(), drawable.getHeight(), true);
        textrenderer2d.setColor(1.0f, 1.0f, 1.0f, 1.0f);

        if (isRenderDebugInfo()) {
            framecount++;
            long diff = System.currentTimeMillis() - timestamp;
            if (diff > 1000) {
                timestamp += diff;
                fps = 1000.0f * (float) framecount / (float) diff;
                framecount = 0;
            }
            textrenderer2d.draw("FPS " + fps, 10, 10);

            double lineHeight = textrenderer2d.getBounds("A").getHeight(); // approximation
            int linePosY = 10;
            int linePosX = drawable.getWidth() - 150;
            for (int i = 4; i >= 0; i--) {
                textrenderer2d.draw(nvStateMgr.getIdentifier(i) + " : " + nvStateMgr.getCount(i), linePosX, linePosY);
                linePosY += lineHeight * 1.2f;
            }
            textrenderer2d.draw("Life State Distribution:", linePosX, linePosY + (int) lineHeight);
        }

        textrenderer2d.endRendering();
    }

    @Override
    public void renderPlace(nvPlace place) {
    }

    @Override
    public void renderTrace(nvTrace trace) {
    }

    @Override
    public void renderOrganic(nvOrganic organic) {
    }

    @Override
    public void init() {
        super.init();

        // Enable VSync
        gl.setSwapInterval(1);
        // Setup the drawing area and shading mode
        gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        gl.glShadeModel(gl.GL_SMOOTH); // try setting this to GL_FLAT and see what happens.
        gl.glEnable(gl.GL_COLOR_MATERIAL);

        System.err.println("INIT GL IS: " + gl.getClass().getName());

        gl.glEnable(gl.GL_COLOR_MATERIAL);
        gl.glEnable(gl.GL_BLEND);
        gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA);
        gl.glEnable(gl.GL_DEPTH_TEST);
        gl.glShadeModel(gl.GL_FLAT);
        enableLighting();
        gluQuadric = glu.gluNewQuadric();

        textrenderer2d = new TextRenderer(new Font("Calibri", Font.PLAIN, 12));
        textrenderer2d.setSmoothing(true);
        textrenderer3d = new TextRenderer(new Font("Calibri", Font.PLAIN, 64));
        textrenderer3d.setSmoothing(true);
    }

    private void disableLighting() {
        gl.glDisable(gl.GL_LIGHTING);
        gl.glDisable(gl.GL_LIGHT0);
    }

    @Override
    public void renderRoot(nvRoot root) {
        gl.glEnable(gl.GL_BLEND);
        gl.glEnable(gl.GL_DEPTH_TEST);

        float radius = simulator.getProjection().geoToVizApprox(5.0f);
        nvVec3 pos = simulator.getProjection().geoToViz(root.getOrganic().getPosition());
        if (frustum.sphereInFrustum(new nvVec3(pos.x, 0, pos.z), radius) != nvFrustum.ALIGN.OUTSIDE) {
            gl.glPushMatrix();
            gl.glTranslatef(pos.x, 0, pos.z);
            gl.glPushMatrix();
            gl.glColor4f(1.0f, 1.0f, 1.0f, 0.5f);
            gl.glRotatef(-90.0f, 1, 0, 0);
            glu.gluDisk(gluQuadric, 0, radius * 3.0f, 12, 1);
            gl.glPopMatrix();
            gl.glTranslatef(0, 0.001f, 0);
            gl.glColor4f(1.0f, 1.0f, 1.0f, 0.5f);
            gl.glRotatef(-90.0f, 1, 0, 0);
            glu.gluDisk(gluQuadric, 0, radius, 12, 1);
            gl.glPopMatrix();
        }
    }

    @Override
    public void renderStem(nvStem stem) {
        gl.glEnable(gl.GL_BLEND);
        gl.glEnable(gl.GL_DEPTH_TEST);

        disableLighting();

        float radius = simulator.getProjection().geoToVizApprox(0.5f);
        nvVec3 pos1 = stem.getSource().getCurrentRenderPos();
        nvVec3 pos2 = stem.getDestination().getCurrentRenderPos();
        if (frustum.sphereInFrustum(new nvVec3(pos1.x, pos1.y, pos1.z), radius) != nvFrustum.ALIGN.OUTSIDE ||
                frustum.sphereInFrustum(new nvVec3(pos2.x, pos2.y, pos2.z), radius) != nvFrustum.ALIGN.OUTSIDE) {
            float width = 2.0f + stem.getNumEntities() / 8.0f;
            if (width > 8.0f) {
                width = 8.0f;
            }
            gl.glLineWidth(width);
            //gl.glColor4f(24.0f/255.0f, 128.0f/255.0f, 203.0f/255.0f, 1.0f);
            gl.glColor4f(1.0f, 1.0f, 1.0f, 0.5f);
            gl.glBegin(gl.GL_LINES);
            gl.glVertex3f(pos1.x, pos1.y, pos1.z);
            gl.glVertex3f(pos2.x, pos2.y, pos2.z);
            gl.glEnd();
            gl.glLineWidth(1.0f);
        }
    }

    @Override
    public void renderNode(nvNode node) {
        gl.glEnable(gl.GL_BLEND);
        gl.glEnable(gl.GL_DEPTH_TEST);

        float radius = simulator.getProjection().geoToVizApprox(2.5f);
        nvVec3 pos = node.getCurrentRenderPos();
        if (frustum.sphereInFrustum(new nvVec3(pos.x, pos.y, pos.z), radius) != nvFrustum.ALIGN.OUTSIDE) {
            enableLighting();
            gl.glPushMatrix();
            gl.glTranslatef(pos.x, pos.y, pos.z);
            float scale = 1.0f + node.getNumEntities() / 25.0f;
            if (scale > 4.0f) {
                scale = 4.0f;
            }
            gl.glScalef(scale, scale, scale);

            if (sphereList1 == -1) {
                sphereList1 = gl.glGenLists(1);
                gl.glNewList(sphereList1, gl.GL_COMPILE_AND_EXECUTE);
                gl.glCullFace(gl.GL_FRONT);
                gl.glPushMatrix();
                gl.glDepthMask(false);
                enableLighting();
                gl.glScalef(1.75f, 1.75f, 1.75f);
                gl.glColor4f(28.0f / 255.0f, 92.0f / 255.0f, 139.0f / 255.0f, 1.0f);
                glu.gluSphere(gluQuadric, radius, 15, 15);
                gl.glCullFace(gl.GL_BACK);
                gl.glDepthMask(true);
                gl.glPopMatrix();
                gl.glEndList();
            } else {
                gl.glCallList(sphereList1);
            }

            if (sphereList2 == -1) {
                sphereList2 = gl.glGenLists(1);
                gl.glNewList(sphereList2, gl.GL_COMPILE_AND_EXECUTE);
                gl.glPushMatrix();
                disableLighting();
                gl.glColor4f(25.0f / 255.0f, 128.0f / 255.0f, 203.0f / 255.0f, 1.0f);
                glu.gluSphere(gluQuadric, radius, 15, 15);
                gl.glPopMatrix();
                gl.glEndList();
            } else {
                gl.glCallList(sphereList2);
            }

            if (sphereList3 == -1) {
                sphereList3 = gl.glGenLists(1);
                gl.glNewList(sphereList3, gl.GL_COMPILE_AND_EXECUTE);
                disableLighting();
                gl.glPushMatrix();
                gl.glColorMask(false, false, false, false);
                gl.glColor4f(25.0f / 255.0f, 128.0f / 255.0f, 203.0f / 255.0f, 1.0f);
                gl.glScalef(1.75f, 1.75f, 1.75f);
                glu.gluSphere(gluQuadric, radius, 15, 15);
                gl.glColorMask(true, true, true, true);
                gl.glPopMatrix();
                gl.glEndList();
            } else {
                gl.glCallList(sphereList3);
            }
            gl.glPopMatrix();

            disableLighting();
            gl.glDepthMask(false);
            gl.glDisable(gl.GL_DEPTH_TEST);

            String text = "" + node.getNumEntities();
            renderBillboardedText(text, pos, true);

            gl.glDepthMask(true);
            gl.glEnable(gl.GL_DEPTH_TEST);
        }
    }

    /*-----------------------------------------------------------------
     * The objects motion is restricted to a rotation on a predefined axis
     * The function bellow does cylindrical billboarding on the Y axis, i.e.
     * the object will be able to rotate on the Y axis only.
     * code based on http://www.lighthouse3d.com/opengl/billboarding/index.php3?billSource
    -----------------------------------------------------------------*/
    private void billboardCylindrical(nvVec3 cam, nvVec3 worldPos) {
        nvVec3 lookAt = new nvVec3(0,0,1);
        nvVec3 objToCamProj = new nvVec3();
        nvVec3 upAux;
        float angleCosine;

        // objToCamProj is the vector in world coordinates from the local origin to the camera
        // projected in the XZ plane
        objToCamProj.x = cam.x - worldPos.x;
        objToCamProj.y = 0;
        objToCamProj.z = cam.z - worldPos.z;

        // normalize both vectors to get the cosine directly afterwards
        objToCamProj.normalize();

        // easy fix to determine wether the angle is negative or positive
        // for positive angles upAux will be a vector pointing in the
        // positive y direction, otherwise upAux will point downwards
        // effectively reversing the rotation.
        upAux = lookAt.cross(objToCamProj);

        // compute the angle
        angleCosine = lookAt.dot(objToCamProj);

        // perform the rotation. The if statement is used for stability reasons
        // if the lookAt and v vectors are too close together then |aux| could
        // be bigger than 1 due to lack of precision
        if ((angleCosine < 0.99990) && (angleCosine > -0.9999)) {
            gl.glRotatef((float) Math.acos(angleCosine) * 180.0f / (float) Math.PI, upAux.x, upAux.y, upAux.z);
        }
    }

    @Override
    public void renderStalk(nvStalk stalk) {
        gl.glEnable(gl.GL_BLEND);
        gl.glEnable(gl.GL_DEPTH_TEST);
        enableLighting();
        float radius = simulator.getProjection().geoToVizApprox(0.5f);
        nvVec3 pos1 = stalk.getSource().getCurrentRenderPos();
        nvVec3 pos2 = stalk.getDestination().getCurrentRenderPos();
        if (frustum.sphereInFrustum(new nvVec3(pos1.x, pos1.y, pos1.z), radius) != nvFrustum.ALIGN.OUTSIDE ||
                frustum.sphereInFrustum(new nvVec3(pos2.x, pos2.y, pos2.z), radius) != nvFrustum.ALIGN.OUTSIDE) {
            float width = 1.0f;
            gl.glLineWidth(width);
            //gl.glColor4f(24.0f/255.0f, 128.0f/255.0f, 203.0f/255.0f, 1.0f);
            gl.glColor4f(1.0f, 1.0f, 1.0f, 0.5f);
            gl.glBegin(gl.GL_LINES);
            gl.glVertex3f(pos1.x, pos1.y, pos1.z);
            gl.glVertex3f(pos2.x, pos2.y, pos2.z);
            gl.glEnd();
            gl.glLineWidth(1.0f);
        }
    }

    @Override
    public void renderLeaf(nvLeaf leaf) {
        gl.glEnable(gl.GL_BLEND);
        gl.glEnable(gl.GL_DEPTH_TEST);

        float radius = simulator.getProjection().geoToVizApprox(3.0f);
        nvVec3 pos = leaf.getCurrentRenderPos();
        if (frustum.sphereInFrustum(new nvVec3(pos.x, pos.y, pos.z), radius) != nvFrustum.ALIGN.OUTSIDE) {

            gl.glPushMatrix();
            gl.glTranslatef(pos.x, pos.y, pos.z);
            gl.glColor4f(leaf.getVizValue(VIZPARAM.APPERANCE_COLOR_R) / 2.0f, leaf.getVizValue(VIZPARAM.APPERANCE_COLOR_G) + 0.5f, leaf.getVizValue(VIZPARAM.APPERANCE_COLOR_B) / 2.0f, 1.0f);
            if (sphereList4 == -1) {
                sphereList4 = gl.glGenLists(1);
                gl.glNewList(sphereList4, gl.GL_COMPILE_AND_EXECUTE);
                //gl.glColor4f(25.0f/255.0f, 128.0f/255.0f, 203.0f/255.0f, 1.0f);
                enableLighting();
                glu.gluSphere(gluQuadric, radius, 12, 12);
                gl.glEndList();
            } else {
                gl.glCallList(sphereList4);
            }
            gl.glPopMatrix();
        }
    }

    @Override
    public void render(){
        // Clear the drawing area
        gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT);
        // Reset the current matrix to the "identity"
        gl.glLoadIdentity();
        super.render();
    }
    
    @Override
    public void renderDataEntity(nvDataEntity entity) {
    }

    @Override
    protected void onPreRenderScene(int renderpass) {
        enableLighting();
    }

    @Override
    protected void onPostRenderScene(int renderpass) {
        // Flush all drawing operations to the graphics card
        gl.glFlush();
    }

    @Override
    protected int getNumRenderPasses() {
        return 1;
    }

    private void enableLighting() {
        gl.glEnable(gl.GL_LIGHTING);
        nvVec3 pos = simulator.getProjection().geoToViz(simulator.getLookAtPosition());
        float lightPos[] = {pos.x, pos.y, pos.z, 0};
        float ambient[] = {0.3f, 0.3f, 0.3f, 1.0f};
        float diffuse[] = {1.0f, 1.0f, 1.0f, 1.0f};
        gl.glLightfv(gl.GL_LIGHT0, gl.GL_POSITION, lightPos, 0);
        gl.glLightfv(gl.GL_LIGHT0, gl.GL_AMBIENT, ambient, 0);
        gl.glLightfv(gl.GL_LIGHT0, gl.GL_DIFFUSE, diffuse, 0);
        gl.glEnable(gl.GL_LIGHT0);
    }

    private void renderBillboardedText(String text, nvVec3 pos, boolean centered) {
        gl.glPushMatrix();
        Rectangle2D bounds = textrenderer3d.getBounds(text);
        float scale = 0.00015f;
        float xtrans = 0.0f;
        float ytrans = 0.0f;
        if (centered) {
            xtrans = -(float) bounds.getCenterX() * scale;
            ytrans = (float) bounds.getCenterY() * scale;
        }
        nvVec3 textPos = new nvVec3(pos.x + xtrans, pos.y + ytrans, pos.z);
        gl.glTranslatef(textPos.x, textPos.y, textPos.z);
        billboardCylindrical(simulator.getProjection().geoToViz(simulator.getCameraPosition()), textPos);
        textrenderer3d.setColor(1, 1, 1, 1);
        textrenderer3d.begin3DRendering();
        textrenderer3d.draw3D(text, 0, 0, 0, scale);
        textrenderer3d.end3DRendering();
        gl.glPopMatrix();
    }

    @Override
    public void postRenderOrganic(nvOrganic organic) {
    }
}

